﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PgBirdie.Core.Config;
using PgBirdie.Core.Dump;
using PgBirdie.Core.Dump.v9_x;
using PgBirdie.Core.Error;
using PgBirdie.Core.Extensions;
using PgBirdie.Core.Helpers;
using PgBirdie.Core.Model;
using PgBirdie.Core.Model.Database.Structure;
using PgBirdie.Core.Service.DbObject;
using PgBirdie.Core.Service.Dump;

namespace PgBirdie.Console.Command
{
    public class DumpCommand : ICommand
    {
        private readonly IDbFromPgDumpService _dbExtract;
        private readonly IDbObjectFromDbService _dbObjectFromDb;
        private readonly IDumpToDiskService _dump;
        private readonly IPgDumpService _pgDump;
        private readonly IRestoreDatabaseSql _restore;

        public DumpCommand(IDbFromPgDumpService dbExtract, IDbObjectFromDbService dbObjectFromDb, IDumpToDiskService dump, IPgDumpService pgDump, IRestoreDatabaseSql restore)
        {
            _dbExtract = dbExtract;
            _dbObjectFromDb = dbObjectFromDb;
            _dump = dump;
            _pgDump = pgDump;
            _restore = restore;
        }

        public ExitCodeType Run()
        {
            AppConsole.WriteLine("Connecting to {0} @ {1}:{2} with role {3}.",
                                 Settings.Source.Database, Settings.Source.Host, Settings.Source.Port,
                                 Settings.Source.Role);

            var rawStructureDump = _pgDump.GetRawStructureFromPgDump(DatabaseType.Source);            

            if (String.IsNullOrEmpty(rawStructureDump))
                return ExitCodeType.Failure;

            var db = _dbExtract.GetDbStructureFromPgDump(rawStructureDump);

            if (db == null || db.Schemas.IsNullOrEmpty())
                return ExitCodeType.Failure;

            CheckThatPrimaryKeysExistForAllTables(db);

            var rawStaticDataDump = _pgDump.GetRawStaticDataFromPgDump(DatabaseType.Source);
            if (!String.IsNullOrWhiteSpace(rawStaticDataDump))
                _dbExtract.PopulateStaticDataFromPgDump(ref db, rawStaticDataDump);

            // Remove old files.
            if (!_dump.PurgeDatabaseDirectory(db))
                return ExitCodeType.Failure;

            if (!_dump.CreateDatabaseDirectory(db))
                return ExitCodeType.Failure;

            // -----------------------------------------------------------------------------------
            // 1. Create database.sql, schema.sql, [table].sql and [sequence].sql files.

            AppConsole.WriteLine("Dumping database {0}.", db.Name);

            var dbSql = _dump.GetDatabaseStructureSql(db);
            _dump.ToDisk(String.Format(@"{0}{1}\database.sql", Settings.PathTo.Dump, db.Name), dbSql);
            var parts = dbSql.Split(new[] {Environment.NewLine}, StringSplitOptions.None).ToList();
            parts.Insert(1, "BEGIN;");
            parts.Insert(0, String.Format("-- DROP DATABASE \"{0}\";", db.Name));
            parts[1] = "--" + parts[1];

            foreach (var schema in db.Schemas.Values.OrderBy(c => c.OrderNo))
            {
                if (!Settings.Source.Schemas.ContainsKey(schema.Name))
                    continue;

                AppConsole.WriteLine("Dumping schema {0}.", schema.Name);

                if (!_dump.CreateSchemaDirectory(schema))
                    return ExitCodeType.Failure;
                if (!_dump.CreateTableStructureDirectory(schema))
                    return ExitCodeType.Failure;
                if (!String.IsNullOrWhiteSpace(rawStaticDataDump) && !_dump.CreateTableStaticDataDirectory(schema))
                    return ExitCodeType.Failure;
                if (!_dump.CreateSequenceDirectory(schema))
                    return ExitCodeType.Failure;

                var schemaSql = _dump.GetSchemaStructureSql(schema);
                if (!_dump.ToDisk(String.Format(@"{0}{1}\{2}\schema.sql", Settings.PathTo.Dump, schema.Database.Name, schema.Name), schemaSql))
                    return ExitCodeType.Failure;

                AppConsole.WriteLine("Dumping sequences from schema {0}.", schema.Name);

                foreach (var sequence in schema.Sequences.Values.OrderBy(c => c.OrderNo))
                {
                    if (!Settings.Source.Schemas[schema.Name].Sequences.ContainsKey(sequence.Name))
                        continue;

                    var sequenceSql = _dump.GetSequenceSql(sequence);
                    if (!_dump.ToDisk(String.Format(@"{0}{1}\{2}\{3}\{4}.sql", Settings.PathTo.Dump, sequence.Schema.Database.Name,
                                     sequence.Schema.Name, Settings.Misc.DirNameForSequences, sequence.Name), sequenceSql))
                        return ExitCodeType.Failure;
                }

                AppConsole.WriteLine("Dumping tables from schema {0}.", schema.Name);

                foreach (var table in schema.Tables.Values.OrderBy(c => c.OrderNo))
                {
                    if (!Settings.Source.Schemas[schema.Name].Tables.ContainsKey(table.Name))
                        continue;

                    var tableSql = _dump.GetTableStructureSql(table);
                    if (!_dump.ToDisk(String.Format(@"{0}{1}\{2}\{3}\{4}.sql", Settings.PathTo.Dump, table.Schema.Database.Name,
                                     table.Schema.Name, Settings.Misc.DirNameForTables, table.Name), tableSql))
                        return ExitCodeType.Failure;

                    if (Settings.Source.Schemas[schema.Name].Tables[table.Name].WithData)
                    {
                        var tableDataSql = _dump.GetTableStaticDataSql(table);
                        if (!_dump.ToDisk(String.Format(@"{0}{1}\{2}\{3}\{4}.sql", Settings.PathTo.Dump, table.Schema.Database.Name,
                                     table.Schema.Name, Settings.Misc.DirNameForStaticData, table.Name), tableDataSql))
                            return ExitCodeType.Failure;
                    }
                }
            }

            // -----------------------------------------------------------------------------------
            // 2. Create restore.sql which can be used to recreate the database.

            var restoreSql = _restore.Get(db);
            _dump.ToDisk(String.Format(@"{0}{1}\restore.sql", Settings.PathTo.Dump, db.Name), restoreSql);

            // -----------------------------------------------------------------------------------
            // 3. Create reset.sql which can be used to purge dynamic data but leaving the static data intact from the database.

            _dump.ToDisk(String.Format(@"{0}{1}\reset.sql", Settings.PathTo.Dump, db.Name), _dump.GetResetSql(db));



            return ExitCodeType.Success;
        }

        private void CheckThatPrimaryKeysExistForAllTables(PgDatabase db)
        {
            var tablesWithoutPrimaryKey = new List<string>();

            var tableNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var configSchema in Settings.Source.Schemas.Values)
                foreach (var configTable in configSchema.Tables.Values)
                    tableNames.Add(String.Format("{0}.{1}", configSchema.Name, configTable.Name));

            var tables = _dbObjectFromDb.GetAllTables(db);

            foreach (var tableName in tableNames)
                if (tables.ContainsKey(tableName) && tables[tableName].PrimaryKey == null)
                    tablesWithoutPrimaryKey.Add(tableName);

            if (tablesWithoutPrimaryKey.Any())
                throw new PgBirdieException(
                    String.Format(
                        "Could not dump tables. All tables should define a primary key before dumping. Tables without primary key:{0}{0}{1}",
                        Environment.NewLine, String.Join(Environment.NewLine, tablesWithoutPrimaryKey)));
        }
    }
}